package org.apache.felix.dm.impl.dependencies.bi;

import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.DependencyManager;
import org.apache.felix.dm.ServiceDependency;
import org.apache.felix.dm.ServiceUtil;
import org.osgi.framework.ServiceReference;

/**
 * Dependency callback manager to be used as a callback instance on which
 * dependencies should be added/removed.
 * 
 * Supported callbacks are: added, swapped and removed and have the
 * following method signatures for the callback methods:
 * <ul>
 * <li>added: void method(ServiceReference reference [optional],
 * ServiceInterface object)(</li>
 * <li>swapped: void method(ServiceReference previousReference, ServiceInterface
 * oldObject, ServiceReference newReference [optional], ServiceInterface
 * newObject)</li>
 * <li>removed: void method(ServiceReference reference [optional],
 * ServiceInterface object)</li>
 * </ul>
 * For ServiceInterface the Service Interface of the expected object can be used
 * (e.g. Model.class) or Object.class.
 */
public class DependencyCallbackManager {

	/*
	 * Map of Component, Map<Service Id, Map<Service Ranking, ServiceTuple>>
	 */
	private Map<Component, Map<Long, Map<Integer, ServiceTuple>>> components = new HashMap<Component, Map<Long, Map<Integer, ServiceTuple>>>();

	private final String removedCallback;

	private final String addedCallback;

	private final String swappedCallback;

	@SuppressWarnings("rawtypes")
	private final Class serviceInterface;

	/**
	 * Create a ServiceDependency with the DependencyManager with the
	 * {@link DependencyCallbackManager} as proxy intercepting the callbacks to
	 * translate the callbacks to the callbacks provided.
	 * 
	 * @param dm
	 *            dependencyManager
	 * @param addedCallback
	 *            add callback
	 * @param swappedCallback
	 *            swap callback
	 * @param removedCallback
	 *            removed callback
	 * @param serviceInterface
	 *            Class of the service being dependent on.
	 * @return
	 */
	public static ServiceDependency createServiceDependency(DependencyManager dm, String addedCallback, String swappedCallback,
			String removedCallback, @SuppressWarnings("rawtypes") Class serviceInterface) {
		return dm.createServiceDependency().setCallbacks(
				new DependencyCallbackManager(addedCallback, swappedCallback, removedCallback, serviceInterface), "added",
				"removed");
	}
	
	public static ServiceDependency createServiceDependency(DependencyManager dm, @SuppressWarnings("rawtypes") Class serviceInterface) {
		return dm.createServiceDependency().setCallbacks(
				new DependencyCallbackManager(null, null, null, serviceInterface), "added",
				"removed");
	}

	private DependencyCallbackManager(String addedCallback, String swappedCallback, String removedCallback,
			@SuppressWarnings("rawtypes") Class serviceInterface) {
		this.addedCallback = addedCallback;
		this.swappedCallback = swappedCallback;
		this.removedCallback = removedCallback;
		this.serviceInterface = serviceInterface;
	}

	@SuppressWarnings("unused")
	private void added(Component component, ServiceReference reference, Object object) {
		if (componentIsDependencyManagerFactory(component)) {
			// component is either aspect or adapter factory instance, these must be ignored.
			return;
		}
		boolean invokeAdded = false;
		Integer ranking = ServiceUtil.getRanking(reference);
		ServiceTuple highestRankedService = null;
		synchronized (components) {
			Long originalServiceId = ServiceUtil.getServiceId(reference);
			Map<Long, Map<Integer, ServiceTuple>> componentMap = components.get(component);
			if (componentMap == null) {
				// create new componentMap
				componentMap = new HashMap<Long, Map<Integer, ServiceTuple>>();
				components.put(component, componentMap);
			}
			Map<Integer, ServiceTuple> rankings = componentMap.get(originalServiceId);
			if (rankings == null) {
				// new component added
				rankings = new HashMap<Integer, ServiceTuple>();
				componentMap.put(originalServiceId, rankings);
				rankings.put(ranking, new ServiceTuple(reference, object));
				invokeAdded = true;
			} else {
				// sanity check, does the actual servicereference already exist in the components
				for (Entry<Integer, ServiceTuple> entry : rankings.entrySet()) {
					if (entry.getValue().getReference().equals(reference)) {
						// something is wrong, this should not happen!!
						System.out.println(">>> INVALID STATE IN DEPENDENCYCALLBACKMANAGER, double add of " + reference);
					}
				}
			}
			
			if (!invokeAdded) {
				highestRankedService = swapHighestRankedService(component, originalServiceId, reference, object, ranking);
			}
		}
		if (invokeAdded) {
			invokeAddedCallback(component, reference, object);
		} else {
			invokeSwappedCallback(component, highestRankedService.getReference(), highestRankedService.getObject(), reference, object);
		}
	}

	@SuppressWarnings("unused")
	private void removed(Component component, ServiceReference reference, Object object) {
		if (componentIsDependencyManagerFactory(component)) {
			// component is either aspect or adapter factory instance, these must be ignored.
			return;
		}
		long serviceId = ServiceUtil.getServiceId(reference);
		// TODO remove this defensive check for DepMan issue Marcel is working on
		if (components.get(component) != null && components.get(component).containsKey(serviceId)) {
			synchronized (components) {
				if (isLastService(component, reference, object, serviceId)) {
					invokeRemovedCallback(component, reference, object);
				}
				Long originalServiceId = ServiceUtil.getServiceId(reference);
				Map<Long, Map<Integer, ServiceTuple>> componentMap = components.get(component);
				if (componentMap != null) {
					Map<Integer, ServiceTuple> rankings = componentMap.get(originalServiceId);
					for (Entry<Integer, ServiceTuple> entry : rankings.entrySet()) {
						if (entry.getValue().getReference().equals(reference)) {
							// remove the reference
							rankings.remove(entry.getKey());
						}
					}
					if (rankings.size() == 0) {
						componentMap.remove(originalServiceId);
					}
					if (componentMap.size() == 0) {
						components.remove(component);
					}
				}
			}
		}
	}

	private ServiceTuple swapHighestRankedService(Component component, Long serviceId, ServiceReference newReference, Object newService, Integer newRanking) {
		// does a component with a higher ranking exists
		synchronized (components) {
			Map<Long, Map<Integer, ServiceTuple>> componentMap = components.get(component);
			Map<Integer, ServiceTuple> rankings = componentMap.get(serviceId);
			Entry<Integer, ServiceTuple> highestEntry = getHighestRankedService(component, serviceId);
			rankings.remove(highestEntry.getKey());
			rankings.put(newRanking, new ServiceTuple(newReference, newService));
			return highestEntry.getValue();
		}
	}

	private Entry<Integer, ServiceTuple> getHighestRankedService(Component component, Long serviceId) {
		Entry<Integer, ServiceTuple> highestEntry = null;
		Map<Long, Map<Integer, ServiceTuple>> componentMap = components.get(component);
		Map<Integer, ServiceTuple> rankings = componentMap.get(serviceId);
		if (rankings != null) {
			for (Entry<Integer, ServiceTuple> entry : rankings.entrySet()) {
				if (highestEntry == null) {
					highestEntry = entry;
				} else {
					if (entry.getKey() > highestEntry.getKey()) {
						highestEntry = entry;
					}
				}
			}
		}
		return highestEntry;
	}

	private boolean componentIsDependencyManagerFactory(Component component) {
		return component.getService() != null && component.getService().getClass().getName().startsWith("org.apache.felix.dm");
	}

	private boolean isLastService(Component component, ServiceReference reference, Object object, long serviceId) {
		// get the collection of rankings
		Map<Long, Map<Integer, ServiceTuple>> componentMap = components.get(component);
		
		Map<Integer, ServiceTuple> rankings = null;
		if (componentMap != null) {
			rankings = componentMap.get(serviceId);
		}
		// if there is only one element left in the collection of rankings
		// and this last element has the same ranking as the supplied service (in other words, it is the same)
		// then this is the last service
		// NOTE: it is possible that there is only one element, but that it's not equal to the supplied service,
		// because an aspect on top of the original service is being removed (but the original service is still
		// there). That in turn triggers:
		// 1) a call to added(original-service)
		// 2) that causes a swap
		// 3) a call to removed(aspect-service) <-- that's what we're talking about
		return (componentMap != null && rankings != null && rankings.size() == 1 && rankings.entrySet().iterator().next().getKey()
				.equals(new Integer(ServiceUtil.getRanking(reference))));
	}

	private void invokeAddedCallback(Component component, ServiceReference serviceReference, Object object) {
		if (addedCallback != null) {
			try {
//				token = SystemStateHolder.getInstance().systemStateService.startWork(object);
				component.invokeCallbackMethod(getInstances(component), addedCallback, new Class[][] { { serviceInterface }, { Object.class },
						{ ServiceReference.class, serviceInterface }, { ServiceReference.class, Object.class } }, new Object[][] { { object }, { object },
						{ serviceReference, object }, { serviceReference, object } });

			} finally {
//				SystemStateHolder.getInstance().systemStateService.endWork(token);
			}
		} else {
			injectDependencyInMember(component, serviceInterface, object);
		}
	}

	private void invokeSwappedCallback(Component component, ServiceReference previousReference, Object previous, ServiceReference currentServiceReference,
			Object current) {
		if (swappedCallback != null) {

			try {
//				token = SystemStateHolder.getInstance().systemStateService.startWork(current);

				component.invokeCallbackMethod(getInstances(component), swappedCallback, new Class[][] { { serviceInterface, serviceInterface },
						{ Object.class, Object.class }, { ServiceReference.class, serviceInterface, ServiceReference.class, serviceInterface },
						{ ServiceReference.class, Object.class, ServiceReference.class, Object.class } }, new Object[][] { { previous, current },
						{ previous, current }, { previousReference, previous, currentServiceReference, current },
						{ previousReference, previous, currentServiceReference, current } });
			} finally {
//				SystemStateHolder.getInstance().systemStateService.endWork(token);
			}
		} else {
			injectDependencyInMember(component, serviceInterface, current);
		}
	}

	private void invokeRemovedCallback(Component component, ServiceReference serviceReference, Object object) {
		if (removedCallback != null) {
			try {
//				token = SystemStateHolder.getInstance().systemStateService.startWork(object);

				component.invokeCallbackMethod(getInstances(component), removedCallback, new Class[][] { { serviceInterface }, { Object.class },
						{ ServiceReference.class, serviceInterface }, { ServiceReference.class, Object.class } }, new Object[][] { { object }, { object },
						{ serviceReference, object }, { serviceReference, object } });
			} finally {
//				SystemStateHolder.getInstance().systemStateService.endWork(token);
			}
		} else {
			injectDependencyInMember(component, serviceInterface, null);
		}
	}
	
    /**
     * Configure a field in the service implementation. The service implementation
     * is searched for fields that have the same type as the class that was specified
     * and for each of these fields, the specified instance is filled in.
     *
     * @param clazz the class to search for
     * @param instance the instance to fill in
     * @param instanceName the name of the instance to fill in, or <code>null</code> if not used
     */
    private void injectDependencyInMember(Component component, Class<?> clazz, Object instance) {
    	Object[] instances = getInstances(component);
    	if (instances != null) {
	    	for (int i = 0; i < instances.length; i++) {
	    		Object serviceInstance = instances[i];
		        Class<?> serviceClazz = serviceInstance.getClass();
		        if (Proxy.isProxyClass(serviceClazz)) {
		            serviceInstance = Proxy.getInvocationHandler(serviceInstance);
		            serviceClazz = serviceInstance.getClass();
		        }
		        while (serviceClazz != null) {
		            Field[] fields = serviceClazz.getDeclaredFields();
		            for (int j = 0; j < fields.length; j++) {
		                Field field = fields[j];
                        Class<?> type = field.getType();
                        if (type.equals(clazz) || type.isAssignableFrom(clazz)) {
		                    try {
		                    	field.setAccessible(true);
		                        // synchronized makes sure the field is actually written to immediately
		                        synchronized (component) {
		                            field.set(serviceInstance, instance);
		                        }
		                    }
		                    catch (Exception e) {
		                        e.printStackTrace();
		                        return;
		                    }
		                }
		            }
		            serviceClazz = serviceClazz.getSuperclass();
		        }
	    	}
    	}
    }
	
	public static Object[] getInstances(Component component) {
		Object[] instances = component.getCompositionInstances();
		Object[] result = new Object[instances.length];
		for (int i = 0; i < instances.length; i++) {
			Object object = instances[i];
			if (Proxy.isProxyClass(object.getClass()) && Proxy.getInvocationHandler(object) instanceof ServiceProxy) {
				result[i] = ((ServiceProxy)Proxy.getInvocationHandler(object)).getTarget();
			} else {
				result[i] = object;
			}
		}
		return result;
	}

	static class ServiceTuple {

		private final ServiceReference reference;
		private final Object object;

		public ServiceTuple(ServiceReference reference, Object object) {
			this.reference = reference;
			this.object = object;

		}

		public ServiceReference getReference() {
			return reference;
		}

		public Object getObject() {
			return object;
		}

	}
}
